﻿//-----------------------------------------------------------------------
// Refer to 
// "A Pretty Good Splash Screen in C#" (http://www.codeproject.com/KB/cs/prettygoodsplashscreen.aspx)
// A splash screen with some neat predictive progress bar features 
//-----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Drawing.Drawing2D;
using System.Collections;
using Microsoft.Win32;
using System.Diagnostics;

namespace SDFL.Windows
{
    /// <summary>
    /// Use this component in your project like this 
    ///     Control.CheckForIllegalCrossThreadCalls = false;         
    ///
    ///     Application.EnableVisualStyles();
    ///     Application.SetCompatibleTextRenderingDefault(false); 
    ///     
    ///     SplashScreen.ShowSplashScreen();
    ///     SplashScreen.SetStatus("Loading module 1");
    ///     System.Threading.Thread.Sleep(1500);
    ///     SplashScreen.SetStatus("Loading module 2");
    ///     System.Threading.Thread.Sleep(500);
    ///     SplashScreen.SetStatus("Loading module 3", false);
    ///     System.Threading.Thread.Sleep(1000);
    ///     SplashScreen.SetStatus("Loading module 3a", false);
    ///     System.Threading.Thread.Sleep(1000);
    ///     SplashScreen.SetStatus("Loading module 3b", false);
    ///     System.Threading.Thread.Sleep(1000);
    ///     SplashScreen.SetStatus("Loading module 3c", false);
    ///     System.Threading.Thread.Sleep(1000);
    ///     SplashScreen.CloseForm();
    ///     Application.Run(new MainForm());
    /// </summary>
    public partial class SplashScreen : Form
    {

        static SplashScreen ms_frmSplash = null;
        static Thread ms_oThread;

        // Fade in and out.
        private double m_dblOpacityIncrement = .05;
        private double m_dblOpacityDecrement = .07;
        private const int TIMER_INTERVAL = 50;

        // Status and progress bar
        static string ms_sStatus;
        private double m_dblCompletionFraction = 0;
        private Rectangle m_rProgress;

        // Progress smoothing
        private double m_dblLastCompletionFraction = 0.0;
        private double m_dblPBIncrementPerTimerInterval = .015;

        // Self-calibration support
        /* How it works? :) 
         * compare the current checkpoint intervals with those saved in registry
         * for example, registry has 
         * Increment = 0.005051
         * Percents = 0 0.0993 0.1678 0.2825 0.3132 0.318 0.3369 0.4054 0.4243 0.4314 0.5083 0.5165 0.8629 0.8652 0.8995 0.9184 
         * firstly, program goes to "SetReferenceInternal", m_dblLastCompletionFraction=0, m_dblCompletionFraction=0.0993;
         * sencondly, program goes to "Timer1.Tick", increase Opacity, m_dblLastCompletionFraction = 0.005051, m_dblCompletionFraction=0.0993, paint progress bar;
         * the thirdly, program goes to "Timer1.Tick", increase Opacity, m_dblLastCompletionFraction = 0.010102, m_dblCompletionFraction=0.0993, paint progress bar;
         * ... 
         * if m_dblLastCompletionFraction > m_dblCompletionFraction(see timer1_Tick()), paint progress bar is paused, 
         * program goes to next "SetReferenceInternal", which will clear the data of m_dblLastCompletionFraction and m_dblCompletionFraction, 
         * assign m_dblLastCompletionFraction=0.0993, m_dblCompletionFraction=0.1678
         * Go to next iteration...
        */
        private bool m_bFirstLaunch = false;
        private DateTime m_dtStart;
        private bool m_bDTSet = false;
        private int m_iIndex = 1;
        private int m_iActualTicks = 0;
        private ArrayList m_alPreviousCompletionFraction;
        private ArrayList m_alActualTimes = new ArrayList();
        private const string REG_KEY_INITIALIZATION = "Initialization";
        private const string REGVALUE_PB_MILISECOND_INCREMENT = "Increment";
        private const string REGVALUE_PB_PERCENTS = "Percents";

        public SplashScreen()
        {
            InitializeComponent();
            this.Opacity = .0;
            timer1.Interval = TIMER_INTERVAL;
            timer1.Start();
            this.ClientSize = this.BackgroundImage.Size;
        }

        // A property returning the splash screen instance
        static public SplashScreen SplashForm
        {
            get
            {
                return ms_frmSplash;
            }
        }

        // Static method for updating the progress percentage.
        // see "You can see how property "Progress" work here."
        static public double Progress
        {
            get
            {
                if (ms_frmSplash != null)
                    return ms_frmSplash.m_dblCompletionFraction;
                return 100.0;
            }
            set
            {
                if (ms_frmSplash != null)
                    ms_frmSplash.m_dblCompletionFraction = value;
            }
        }

        // A static method to create the thread and 
        // launch the SplashScreen.
        static public void ShowSplashScreen()
        {
            // Make sure it's only launched once.
            if (ms_frmSplash != null)
            {
                return;
            }

            ms_oThread = new Thread(new ThreadStart(ShowForm));

            ms_oThread.IsBackground = true;
            ms_oThread.SetApartmentState(ApartmentState.STA);
            ms_oThread.Start();

        }

        // A static method to close the SplashScreen
        static public void CloseForm()
        {
            if (ms_frmSplash != null && ms_frmSplash.IsDisposed == false)
            {
                // Make it start going away.
                ms_frmSplash.m_dblOpacityIncrement = -ms_frmSplash.m_dblOpacityDecrement;
            }

            // we don't need these any more.
            ms_oThread = null;
            ms_frmSplash = null;
        }

        // A static method to set the status and update the reference.
        static public void SetStatus(string newStatus)
        {
            SetStatus(newStatus, true);
        }


        /// <summary>
        ///  A static method to set the status and optionally update the reference.
        ///  
        ///  This is useful if you are in a section of code that has a variable
        ///  set of status string updates.  In that case, don't set the reference.
        ///  
        ///  That's to say, you should use "false" in this case:
        ///     SplashScreen.SetStatus("Loading module 14", false);
        ///     System.Threading.Thread.Sleep(1000);
        ///     SplashScreen.SetStatus("Loading module 14a", false);
        ///     System.Threading.Thread.Sleep(1000);
        ///     SplashScreen.SetStatus("Loading module 14b", false);
        ///     System.Threading.Thread.Sleep(1000);
        ///     SplashScreen.SetStatus("Loading module 14c", false);
        ///     
        ///  if m_dblLastCompletionFraction go up to larger than m_dblCompletionFraction(see timer1_Tick()), 
        ///  SplashScreen.SetStatus("...", false); will not update reference, 
        ///  From UI, progress bar is paused, cusomer know this work "Loading module 14, 14a, 14b, 14c" is not finished.
        /// </summary>
        /// <param name="newStatus"></param>
        /// <param name="setReference"></param>
        static public void SetStatus(string newStatus, bool setReference)
        {
            ms_sStatus = newStatus;
            if (ms_frmSplash == null)
                return;
            if (setReference)
                ms_frmSplash.SetReferenceInternal();
        }

        // Static method called from the initializing application to 
        // give the splash screen reference points.  Not needed if
        // you are using a lot of status strings.
        static public void SetReferencePoint()
        {
            if (ms_frmSplash == null)
                return;
            ms_frmSplash.SetReferenceInternal();

        }

        #region ********* Event Handlers **********
        // Paint the portion of the panel invalidated during the tick event.
        private void pnlStatus_Paint(object sender, PaintEventArgs e)
        {
            // there is an issue? if first launch, pnlStatus can't be changed, if(){} not be executed
            if (m_bFirstLaunch == false && e.ClipRectangle.Width > 0 && m_iActualTicks > 1)
            {
                LinearGradientBrush brBackground = new LinearGradientBrush(m_rProgress, Color.FromArgb(158, 96, 56), Color.FromArgb(181, 237, 254), LinearGradientMode.Horizontal);
                e.Graphics.FillRectangle(brBackground, m_rProgress);
            }

        }

        // Tick Event handler for the Timer control.  Handle fade in and fade out.  Also
        // handle the smoothed progress bar.
        private void timer1_Tick(object sender, EventArgs e)
        {
            lblStatus.Text = ms_sStatus;

            if (m_dblOpacityIncrement > 0)
            {
                // fade in
                m_iActualTicks++;
                if (this.Opacity < 1)
                    this.Opacity += m_dblOpacityIncrement;
            }
            else
            {
                // fade out
                if (this.Opacity > 0)
                    this.Opacity += m_dblOpacityIncrement;
                else
                {
                    // fade out fininshed
                    StoreIncrements();
                    this.Close();
                    Debug.WriteLine("Called this.Close()");
                }

            }

            // You can see how property "Progress" work here.
            //if (Progress < 100)
            //{
            //    Progress += 5;

            //    int width = (int)Math.Floor(pnlStatus.ClientRectangle.Width * Progress / 100.0);
            //    int height = pnlStatus.ClientRectangle.Height;
            //    int x = pnlStatus.ClientRectangle.X;
            //    int y = pnlStatus.ClientRectangle.Y;
            //    if (width > 0 && height > 0)
            //    {
            //        m_rProgress = new Rectangle(x, y, width, height);
            //        pnlStatus.Invalidate(m_rProgress);
            //    }
            //}

            // there is an issue? if first launch, no registry key, pnlStatus can't be changed, if(){} not be executed
            if (m_bFirstLaunch == false && m_dblLastCompletionFraction < m_dblCompletionFraction)
            {
                Debug.WriteLine(string.Format("last value: {0}; current value: {1}", m_dblCompletionFraction, m_dblLastCompletionFraction));
                m_dblLastCompletionFraction += m_dblPBIncrementPerTimerInterval;
                int width = (int)Math.Floor(pnlStatus.ClientRectangle.Width * m_dblLastCompletionFraction);
                int height = pnlStatus.ClientRectangle.Height;
                int x = pnlStatus.ClientRectangle.X;
                int y = pnlStatus.ClientRectangle.Y;
                if (width > 0 && height > 0)
                {
                    m_rProgress = new Rectangle(x, y, width, height);
                    pnlStatus.Invalidate(m_rProgress);
                    int iSecondsLeft = 1 + (int)(TIMER_INTERVAL * ((1.0 - m_dblLastCompletionFraction) / m_dblPBIncrementPerTimerInterval)) / 1000;
                    if (iSecondsLeft == 1)
                        lblTimeRemaining.Text = string.Format("1 second remaining");
                    else
                        lblTimeRemaining.Text = string.Format("{0} seconds remaining", iSecondsLeft);

                }
            }
        }
        #endregion

        #region ********* Private methods *********

        // A private entry point for the thread.       
        private static void ShowForm()
        {
            ms_frmSplash = new SplashScreen();
            Application.Run(ms_frmSplash);
        }
         
        /// <summary>
        /// Internal method for setting reference points.
        /// Main purpose: 
        ///    1. log a checkpoint time, 
        ///    2. update reference (m_dblLastCompletionFraction and m_dblCompletionFraction)
        ///           m_dblLastCompletionFraction = m_dblCompletionFraction;
        ///           m_dblCompletionFraction = m_alPreviousCompletionFraction[m_iIndex++](values from registry)
        /// 
        /// </summary>
        private void SetReferenceInternal()
        {
            if (m_bDTSet == false)
            {
                m_bDTSet = true;
                m_dtStart = DateTime.Now;
                // try to read the checkpoint intervals from the previous invocation of the splashscreen from the registry
                // if no invocation before, m_alPreviousCompletionFraction.Count = 0
                ReadIncrements();
            }

            double dblMilliseconds = ElapsedMilliSeconds();
            // *****m_alActualTimes will hold elapsed time between every SetReferenceInternal()
            m_alActualTimes.Add(dblMilliseconds);
            m_dblLastCompletionFraction = m_dblCompletionFraction;
            if (m_alPreviousCompletionFraction != null && m_iIndex < m_alPreviousCompletionFraction.Count)
                m_dblCompletionFraction = (double)m_alPreviousCompletionFraction[m_iIndex++];
            else
                // if no invocation before, m_dblCompletionFraction = 1
                m_dblCompletionFraction = (m_iIndex > 0) ? 1 : 0;
        }

        // Utility function to return elapsed Milliseconds since the 
        // SplashScreen was launched.
        private double ElapsedMilliSeconds()
        {
            TimeSpan ts = DateTime.Now - m_dtStart;
            return ts.TotalMilliseconds;
        }

        // Function to read the checkpoint intervals from the previous invocation of the
        // splashscreen from the registry.
        private void ReadIncrements()
        {
            string sPBIncrementPerTimerInterval = RegistryAccess.GetStringRegistryValue(REGVALUE_PB_MILISECOND_INCREMENT, "0.0015");
            double dblResult;

            if (Double.TryParse(sPBIncrementPerTimerInterval, System.Globalization.NumberStyles.Float, System.Globalization.NumberFormatInfo.InvariantInfo, out dblResult) == true)
                m_dblPBIncrementPerTimerInterval = dblResult;
            else
                m_dblPBIncrementPerTimerInterval = .0015;

            string sPBPreviousPctComplete = RegistryAccess.GetStringRegistryValue(REGVALUE_PB_PERCENTS, "");

            if (sPBPreviousPctComplete != "")
            {
                string[] aTimes = sPBPreviousPctComplete.Split(null);
                m_alPreviousCompletionFraction = new ArrayList();

                for (int i = 0; i < aTimes.Length; i++)
                {
                    double dblVal;
                    if (Double.TryParse(aTimes[i], System.Globalization.NumberStyles.Float, System.Globalization.NumberFormatInfo.InvariantInfo, out dblVal))
                        m_alPreviousCompletionFraction.Add(dblVal);
                    else
                        m_alPreviousCompletionFraction.Add(1.0);
                }
            }
            else
            {
                m_bFirstLaunch = true;
                lblTimeRemaining.Text = "";
            }
        }

        // Method to store the intervals (in percent complete) from the current invocation of
        // the splash screen to the registry.
        private void StoreIncrements()
        {
            string sPercent = "";
            
            // this is the whole time since the SplashScreen was launched. 
            double dblElapsedMilliseconds = ElapsedMilliSeconds();

            // calculate fininshed percentage on every checkpoint interval
            for (int i = 0; i < m_alActualTimes.Count; i++)
                sPercent += ((double)m_alActualTimes[i] / dblElapsedMilliseconds).ToString("0.####", System.Globalization.NumberFormatInfo.InvariantInfo) + " ";
            
            // save the percentage list
            RegistryAccess.SetStringRegistryValue(REGVALUE_PB_PERCENTS, sPercent);

            // save the new value of IncrementPerTimerInterval as Self-calibration
            m_dblPBIncrementPerTimerInterval = 1.0 / (double)m_iActualTicks;
            RegistryAccess.SetStringRegistryValue(REGVALUE_PB_MILISECOND_INCREMENT, m_dblPBIncrementPerTimerInterval.ToString("#.000000", System.Globalization.NumberFormatInfo.InvariantInfo));
        }
        #endregion        
    }

    /// <summary>
    /// A class for managing registry access.
    /// </summary>
    public class RegistryAccess
    {
        private const string SOFTWARE_KEY = "Software";
        private const string COMPANY_NAME = "MyCompany";
        private const string APPLICATION_NAME = "MyApplication";

        // Method for retrieving a Registry Value.
        static public string GetStringRegistryValue(string key, string defaultValue)
        {
            RegistryKey rkCompany;
            RegistryKey rkApplication;

            rkCompany = Registry.CurrentUser.OpenSubKey(SOFTWARE_KEY, false).OpenSubKey(COMPANY_NAME, false);
            if (rkCompany != null)
            {
                rkApplication = rkCompany.OpenSubKey(APPLICATION_NAME, true);
                if (rkApplication != null)
                {
                    foreach (string sKey in rkApplication.GetValueNames())
                    {
                        if (sKey == key)
                        {
                            return (string)rkApplication.GetValue(sKey);
                        }
                    }
                }
            }
            return defaultValue;
        }

        // Method for storing a Registry Value.
        static public void SetStringRegistryValue(string key, string stringValue)
        {
            RegistryKey rkSoftware;
            RegistryKey rkCompany;
            RegistryKey rkApplication;

            rkSoftware = Registry.CurrentUser.OpenSubKey(SOFTWARE_KEY, true);
            rkCompany = rkSoftware.CreateSubKey(COMPANY_NAME);
            if (rkCompany != null)
            {
                rkApplication = rkCompany.CreateSubKey(APPLICATION_NAME);
                if (rkApplication != null)
                {
                    rkApplication.SetValue(key, stringValue);
                }
            }
        }
    }
}
